<!--
The MIT License (MIT)

Copyright (c) 2013 bill@bunkat.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->

<!--

d3.js swimlane chart example adapted to use in Emscripten from http://bl.ocks.org/bunkat/1962173

-->

<html><head>
<meta charset="UTF-8">
<title>Emscripten toolchain profiler results</title>
<script src="https://d3js.org/d3.v3.min.js"></script>
<style>
.chart { shape-rendering: crispEdges; }
.mini text { font: 9px sans-serif; }
.main text { font: 12px sans-serif; }
.month text { text-anchor: start; }
.todayLine { stroke: blue; stroke-width: 1.5; }
.axis line, .axis path { stroke: black; }
.miniItem { stroke-width: 6; }
.future { stroke: gray; fill: #ddd; }
.past { stroke: green; }
.brush .extent { stroke: gray; fill: blue; fill-opacity: .165; }
div.tooltip {
    position: absolute;
    text-align: center;
    padding: 2px;
    font: 12px sans-serif;
    background: lightsteelblue;
    border: 0px;
    border-radius: 8px;
    pointer-events: none;
}
</style>
</head><body>

<p>Show only blocks that took at least <input id='hideBlocksSmallerThan' value='10'></input> msecs (0=show all).<input type='button' value='Refresh' onclick='refresh()'></input> To zoom in, grab with mouse from either the far left or right ends of the blue/purple background rectangle, and drag the selection area smaller.

<script type="text/javascript">

function getValueOfParam(param) {
  var e = location.search.substr(1).split('&');
  for(var i in e) {
    var p = e[i].split('=');
    if (p.length == 2 && p[0] == param) return p[1];
  }
}

if (location.search.indexOf('hideBlocksSmallerThan') != -1) {
  document.getElementById('hideBlocksSmallerThan').value = parseInt(getValueOfParam('hideBlocksSmallerThan'));
}

function refresh() {
  var hideBlocksSmallerThan = parseInt(document.getElementById('hideBlocksSmallerThan').value);
  var url = window.location.href;
  if (url.indexOf('?') != -1) url = url.split('?')[0]
  window.location.href = url + '?hideBlocksSmallerThan=' + hideBlocksSmallerThan;
}

// Dataset elements for d3.js: 'lanes' specify the rows, 'items' specify the individual blocks on the lanes.
var lanes = [];
var items = [];

function pathBasename(absPath) {
  return absPath.substr(Math.max(absPath.lastIndexOf('\\'), absPath.lastIndexOf('/'))+1);
}

function findPositionalParams(cmdLine, argsTakingParams) {
  var np = [];
  for(var i = 1; i < cmdLine.length; ++i) {
    var perhapsKey = cmdLine[i-1];
    var value = cmdLine[i];
    if (value[0] != '-' && value[0] != '@' && argsTakingParams.indexOf(perhapsKey) == -1) {
      np.push(value);
    }
  }
  return np;
}

function cmdLineBasename(cmdLine) {
  var cmd = cmdLine[0];
  cmdname = pathBasename(cmd);

  // cmdline could be '/path/to/node --param /path/to/file/to/run', in which case
  // we want to return 'run' as the interesting command that was executed
  if (cmdname.indexOf('node') == 0 || cmdname.indexOf('python') == 0) {
    var positionalParams = findPositionalParams(cmdLine, []);
    if (positionalParams.length > 0) return cmdLineBasename(positionalParams);
  }
  return cmdname;
}

function shortenRidiculouslyLongCmdLine(cmdLine) {
  var cmdLine2 = [];
  for(var l of cmdLine) {
    if (l.length > 100) {
      cmdLine2.push(l.substr(0, 50) + ' ... ' + l.substr(l.length - 50));
    } else {
      cmdLine2.push(l);
    }
  }
  return cmdLine2;
}

function htmlFormatCmdLine(cmdLine) {
  cmdLine = shortenRidiculouslyLongCmdLine(cmdLine);
  var html = '';
  var prev = '';
  var lineLength = 0;
  for(var l of cmdLine) {
    if (prev.length > 3 && (prev[0] != '-' || lineLength >= 100)) {
      html += '<br>';
      lineLength = 0;
    }
    else {
      html += ' ';
      ++lineLength;
    }
    html += l;
    prev = l;
    lineLength += l.length;
  }
  return html.trim();
}

function findValueOfParam(cmdLine, key) {
  var i = cmdLine.indexOf(key);
  if (i != -1) return cmdLine[i+1];
}

function findInterestingParams(cmdLine, interestingParams) {
  p = [];
  for(var i in interestingParams) {
    if (cmdLine.indexOf(interestingParams[i]) != -1)
      p.push(interestingParams[i]);
  }
  return p;
}

function startsWith(str, substr) {
  return str.indexOf(substr) == 0;
}

// A heuristic approach to finding print-worthy elements from a given command line array.
function findInterestingBits(cmdLine) {
  var basename = cmdLineBasename(cmdLine);
  if (startsWith(basename, 'em++') || startsWith(basename, 'emcc')) {
    src = findValueOfParam(cmdLine, '-c');
    if (src) src = '-c ' + pathBasename(src);
    else {
      src = findValueOfParam(cmdLine, '-o');
      if (src) src = '-o ' + pathBasename(src);
    }
    return [basename, src];
  } else if (startsWith(basename, 'clang') || startsWith(basename, 'clang++')) {
    var posParams = findPositionalParams(cmdLine, ['-target', '-o']);
    for(var i = 0; i < posParams.length; ++i) {
      if (posParams[i].indexOf('.c') != -1 && (posParams[i].indexOf('/') != -1 || posParams[i].indexOf('\\') != -1))
        posParams[i] = pathBasename(posParams[i]);
    }
    var out = findValueOfParam(cmdLine, '-o');
    if (out) out = '-o ' + pathBasename(out);
    if (cmdLine.length <= 3) return [basename].concat(cmdLine.slice(1));
    return [basename].concat(posParams).concat([out]);
  } else if (startsWith(basename, 'llvm-ar') || startsWith(basename, 'optimizer') || startsWith(basename, 'asm2wasm') || startsWith(basename, 'wasm-as')) {
    for(var i = 0; i < cmdLine.length; ++i) {
      if (cmdLine[i].indexOf('/') != -1 || cmdLine[i].indexOf('\\') != -1)
        cmdLine[i] = pathBasename(cmdLine[i]);
    }
    return cmdLine;
  } else if (startsWith(basename, 'opt')) {
    var interestingParams = findInterestingParams(cmdLine, ['-O0', '-O1', '-O2', '-O3', '-Os', '-Oz'])
    var out = findValueOfParam(cmdLine, '-o');
    if (out) out = '-o ' + pathBasename(out);
    return [basename].concat(interestingParams).concat([out]);
  } else if (startsWith(basename, 'llvm-nm')) {
    var posParams = findPositionalParams(cmdLine, []);
    for(var i = 0; i < posParams.length; ++i) {
      if (posParams[i].indexOf('/') != -1 || posParams[i].indexOf('\\') != -1)
        posParams[i] = pathBasename(posParams[i]);
    }
    return [basename].concat(posParams);
  } else if (startsWith(basename, 'llc')) {
    var interestingParams = findInterestingParams(cmdLine, ['-O0', '-O1', '-O2', '-O3', '-Os', '-Oz'])
    var posParams = findPositionalParams(cmdLine, ['-o']);
    for(var i = 0; i < posParams.length; ++i) {
      if (posParams[i].indexOf('/') != -1 || posParams[i].indexOf('\\') != -1)
        posParams[i] = pathBasename(posParams[i]);
    }
    var ret = [basename].concat(interestingParams).concat(posParams);
    var out = findValueOfParam(cmdLine, '-o');
    if (out) {
      out = '-o ' + pathBasename(out);
      if (out.indexOf('tmp') != 0)
        ret.concat([out]);
    }
    return ret;
  } else if (startsWith(basename, 'compiler.js')) {
    var posParams = findPositionalParams(cmdLine, []);
    var interestingParams = [];
    for(var i = 1; i < posParams.length; ++i) {
      if (posParams[i].indexOf('/') != -1 || posParams[i].indexOf('\\') != -1)
        posParams[i] = pathBasename(posParams[i]);
      interestingParams.push(posParams[i]);
    }
    return [basename].concat(interestingParams);
  } else if (startsWith(basename, 'js-optimizer.js')) {
    var posParams = findPositionalParams(cmdLine, []);
    var interestingParams = [];
    for(var i = 1; i < posParams.length; ++i) {
      if (posParams[i].indexOf('/') != -1 || posParams[i].indexOf('\\') != -1)
        posParams[i] = pathBasename(posParams[i]);
      if (posParams[i].indexOf('tmp') != 0)
        interestingParams.push(posParams[i]);
    }
    return [basename].concat(interestingParams);
  } else {
    return [basename];
  }
}

// Time extents of the times on the swimlane
var firstStartTime = Infinity;
var lastEndTime = -Infinity;

function createSwimlaneChart(data) {
  var itemsByPid = {};

  var t0 = Infinity;
  for(var i in data) {
    t0 = Math.min(t0, data[i].time)
    lastEndTime = Math.max(lastEndTime, data[i].time);
  }
  firstStartTime = t0;

  var itemsOrdered = [];

  var itemStackByPid = {};
  function pushItemToStack(item, pid) {
    if (!itemStackByPid[pid]) itemStackByPid[pid] = [item];
    else {
      var parent = topOnStack(pid);
      if (parent) {
        parent.children.push(item);
      }
      itemStackByPid[pid].push(item);
    }
  }
  function topOnStack(pid) {
    if (!itemStackByPid[pid]) return null;
    return itemStackByPid[pid][itemStackByPid[pid].length-1];
  }
  function popItemFromStack(item, pid) {
    var stack = itemStackByPid[pid];
    for(var i = stack.length-1; i >= 0; --i)
      if (stack[i] === item) {
        stack.splice(i, 1);
        return;
      }
  }

  var startOrder = 0; // Tiebreaker for sorting predicate
  for(var i in data) {
    var d = data[i];
    if (d.op === 'start') { // A top-level tool process has started
      itemsByPid[d.pid] = {
        pid: d.pid,
        start: (d.time - t0)*1000,
        end: null,
        startOrder: startOrder++,
        cmdLine: d.cmdLine,
        cmd: findInterestingBits(d.cmdLine).join(' '),
        color: d3.rgb('#80ff80'),
        parent: null,
        children: []
      };
      pushItemToStack(itemsByPid[d.pid], d.pid);
    } else if (d.op === 'exit') { // A top-level tool process has finished
      if (itemsByPid[d.pid]) {
        itemsByPid[d.pid].end = (d.time - t0)*1000;
        d.end = (d.time - t0)*1000;
        itemsOrdered.push(itemsByPid[d.pid]);
        popItemFromStack(itemsByPid[d.pid], d.pid);
        delete itemsByPid[d.pid];
      } else {
        console.error('"exit" record seen for PID ' + d.pid + ', but no corresponding "start" record present!');
      }
    } else if (d.op === 'spawn') { // A subprocess was spawned
      itemsByPid[d.targetPid] = {
        parentPid: d.pid,
        pid: d.targetPid,
        start: (d.time - t0)*1000,
        end: null,
        startOrder: startOrder++,
        cmdLine: d.cmdLine,
        cmd: findInterestingBits(d.cmdLine).join(' '),
        color: d3.rgb('#80ff80'),
        parent: topOnStack(d.pid),
        children: []
      };
      pushItemToStack(itemsByPid[d.targetPid], d.pid);
      pushItemToStack(itemsByPid[d.targetPid], d.targetPid);
    } else if (d.op === 'finish') { // A subprocess has finished
      if (itemsByPid[d.targetPid]) {
        itemsByPid[d.targetPid].end = (d.time - t0)*1000;
        itemsOrdered.push(itemsByPid[d.targetPid]);
        popItemFromStack(itemsByPid[d.targetPid], d.pid);
        popItemFromStack(itemsByPid[d.targetPid], d.targetPid);
        delete itemsByPid[d.targetPid];
      } else {
        console.error('"finish" record seen for PID ' + d.targetPid + ', but no corresponding "spawn" record present!');
      }
    } else if (d.op === 'enterBlock') { // A custom profiling block has been stepped into
      var id = d.pid + '-' + d.name + '-' + d.subprocessPid;
      itemsByPid[id] = {
        pid: d.pid,
        start: (d.time - t0)*1000,
        end: null,
        startOrder: startOrder++,
        cmd: d.name,
        color: d3.rgb('#8080ff'),
        parent: topOnStack(d.pid),
        children: []
      };
      pushItemToStack(itemsByPid[id], d.pid);
    } else if (d.op === 'exitBlock') { // A custom profiling block has been exited
      var id = d.pid + '-' + d.name + '-' + d.subprocessPid;
      if (itemsByPid[id]) {
        itemsByPid[id].end = (d.time - t0)*1000;
        itemsOrdered.push(itemsByPid[id]);
        popItemFromStack(itemsByPid[id], d.pid);
        delete itemsByPid[id];
      } else {
        console.error('"exitBlock" record seen for PID "' + id + '", but no corresponding "enterBlock" record present!');
      }
    }
  }

  // Need to have siblings in proceeding time order so that they appear on the lanes correctly.
  for(var i in itemsByPid) itemsOrdered.push(itemsByPid[i]);
  itemsOrdered.sort(function(a, b) { if (a.start == b.start) return a.startOrder - b.startOrder; else return a.start - b.start;});

  // Specifies for each lane (row) how far to the right it is used.
  var laneOccupancy = [];
  var lanes = [];

  // Tests whether the whole tree of items, with its root/parent node 'item', fits if
  // the root 'item' is placed on lane index 'lane' (and the children placed on the subsequent lanes underneath)
  function itemHierarchyFitsRootedOnLane(item, lane) {
    var i = item;
    while(i && lane < laneOccupancy.length) {
      if (i.start < laneOccupancy[lane]) return false;
      ++lane;
      i = i.children[0];
    }
    return true;
  }
  function fitLaneHierarchy(item) {
    var parent = item.parent;
    var highestLaneAllowed = 0;
    // Child should never be drawn on the same or above lane as its parent.
    if (parent && !parent.visualBlock) {
      console.error('Item "' + item.cmd + '" will possibly show up detached from its parent chain, because its parent "' + parent.cmd + '" was hidden from view.');
    }
    if (parent && parent.visualBlock) highestLaneAllowed = parent.visualBlock.lane+1;
    for(var lane = highestLaneAllowed; lane < laneOccupancy.length; ++lane) {
      if (itemHierarchyFitsRootedOnLane(item, lane)) {
        laneOccupancy[lane] = item.visualEnd;
        return lane;
      }
    }
    laneOccupancy.push(item.visualEnd);
    lanes.push({
      id: laneOccupancy.length-1,
      label: laneOccupancy.length-1
    });
    return laneOccupancy.length-1;
  }

  // Filter items that took too little time and should not be displayed.
  var hideBlocksSmallerThan = parseInt(document.getElementById('hideBlocksSmallerThan').value);
  for(var i = 0; i < itemsOrdered.length; ++i) {
    var p = itemsOrdered[i];
    var duration = p.end - p.start;
    if (duration < hideBlocksSmallerThan) { // Should this be hidden?
      // removing this node, so connect the parent and children.
      var parent = p.parent;
      var children = p.children;

      // Disconnect this node from the child list of its parent, and root up all children of this
      // to be children of the parent, but be careful to preserve position, i.e. the children should
      // appear in the place in array where the current node was.
      if (parent) {
        var parentIdx = parent.children.indexOf(p);
        if (parentIdx < 0) throw 'internal error: inconsistent tree structure!';
        var head = parent.children.slice(0, parentIdx);
        var tail = parent.children.slice(parentIdx + 1, parent.children.length);
        parent.children = head.concat(children).concat(tail);
      }

      // Update the parent node of each child.
      for(var c in children) children[c].parent = parent;

      itemsOrdered.splice(i, 1);
      --i;
    }
  }

  var id = 0;
  for(var i in itemsOrdered) {
    var p = itemsOrdered[i];

    var label = '(' + (p.end - p.start).toFixed(0) + 'ms) ' + p.cmd;
    p.visualEnd = Math.max(p.end, p.start + label.length*14);
    var laneIndex = fitLaneHierarchy(p);

    var item = {
      class: 'past',
      desc: 'Description',
      start: p.start,
      end: p.end,
      id: id,
      lane: laneIndex,
      cmdLine: p.cmdLine,
      cmd: p.cmd,
      color: p.color,
      label: label
    };
    p.visualBlock = item;
    items.push(item);
    ++id;
  }

  var margin = {top: 20, right: 15, bottom: 15, left: 60};
  var width = document.body.clientWidth - margin.left - margin.right - 50;
  var height = lanes.length * 30;
  var miniHeight = lanes.length*7;
  var mainHeight = height - miniHeight - 50;

  var x = d3.scale.linear().domain([
            d3.min(items, function(d) { return d.start; }),
            d3.max(items, function(d) { return d.end; })])
    .range([0, width]);

  var x1 = d3.scale.linear().range([0, width]);

  var ext = d3.extent(lanes, function(d) { return d.id; });
  var y1 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, mainHeight]);
  var y2 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, miniHeight]);

  var chart = d3.select('body')
    .append('svg:svg')
    .attr('width', width + margin.right + margin.left)
    .attr('height', height + margin.top + margin.bottom)
    .attr('class', 'chart');

  chart.append('defs').append('clipPath')
    .attr('id', 'clip')
    .append('rect')
    .attr('width', width)
    .attr('height', mainHeight);

  var mini = chart.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
    .attr('width', width)
    .attr('height', miniHeight)
    .attr('class', 'mini');

  var main = chart.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + (margin.top + miniHeight + 20) + ')')
    .attr('width', width)
    .attr('height', mainHeight)
    .attr('class', 'main');

  // Define the div for the tooltip
  var div = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);

  // draw the lanes for the main chart
  main.append('g').selectAll('.laneLines')
    .data(lanes)
    .enter().append('line')
    .attr('x1', 0)
    .attr('y1', function(d) { return d3.round(y1(d.id)) + 0.5; })
    .attr('x2', width)
    .attr('y2', function(d) { return d3.round(y1(d.id)) + 0.5; })
    .attr('stroke', function(d) { return d.label === '' ? 'white' : 'lightgray' });

  main.append('g').selectAll('.laneText')
    .data(lanes)
    .enter().append('text')
    .text(function(d) { return d.label; })
    .attr('x', -10)
    .attr('y', function(d) { return y1(d.id + .5); })
    .attr('dy', '0.5ex')
    .attr('text-anchor', 'end')
    .attr('class', 'laneText');

  // draw the lanes for the mini chart
  mini.append('g').selectAll('.laneLines')
    .data(lanes)
    .enter().append('line')
    .attr('x1', 0)
    .attr('y1', function(d) { return d3.round(y2(d.id)) + 0.5; })
    .attr('x2', width)
    .attr('y2', function(d) { return d3.round(y2(d.id)) + 0.5; })
    .attr('stroke', function(d) { return d.label === '' ? 'white' : 'lightgray' });

  mini.append('g').selectAll('.laneText')
    .data(lanes)
    .enter().append('text')
    .text(function(d) { return d.label; })
    .attr('x', -10)
    .attr('y', function(d) { return y2(d.id + .5); })
    .attr('dy', '0.5ex')
    .attr('text-anchor', 'end')
    .attr('class', 'laneText');

  // draw the x axis

  var xDateAxis = d3.svg.axis()
    .scale(x)
    .orient('bottom')
    .tickFormat(function(e) { return e + ' ms' })
    .tickSize(6, 0, 0);

  var x1DateAxis = d3.svg.axis()
    .scale(x1)
    .orient('bottom')
    .tickFormat(function(e) { return e + ' ms' })
    .tickSize(6, 0, 0);

  main.append('g')
    .attr('transform', 'translate(0,' + mainHeight + ')')
    .attr('class', 'main axis')
    .call(x1DateAxis);

  mini.append('g')
    .attr('transform', 'translate(0,' + miniHeight + ')')
    .attr('class', 'axis')
    .call(xDateAxis);

  // draw the items
  var itemRects = main.append('g')
    .attr('clip-path', 'url(#clip)');

  // generates a single path for each item class in the mini display
  // ugly - but draws mini 2x faster than append lines or line generator
  // is there a better way to do a bunch of lines as a single path with d3?
  function getPaths(items) {
    var paths = {}, d, offset = .5 * y2(1) + 0.5, result = [];
    for (var i = 0; i < items.length; i++) {
      d = items[i];
      if (!paths[d.class]) paths[d.class] = '';
      paths[d.class] += ['M',x(d.start),(y2(d.lane) + offset),'H',x(d.end)].join(' ');
    }

    for (var className in paths) {
      result.push({class: className, path: paths[className]});
    }

    return result;
  }

  mini.append('g').selectAll('miniItems')
    .data(getPaths(items))
    .enter().append('path')
    .attr('class', function(d) { return 'miniItem ' + d.class; })
    .attr('d', function(d) { return d.path; });

  // draw the selection area
  var brush = d3.svg.brush()
    .x(x)
    .extent([
         d3.min(items, function(d) { return d.start; }),
         d3.max(items, function(d) { return d.end; })])
    .on("brush", display);

  mini.append('g')
    .attr('class', 'x brush')
    .call(brush)
    .selectAll('rect')
      .attr('y', 1)
      .attr('height', miniHeight - 1);

  mini.selectAll('rect.background').remove();
  display();

  function display () {
    var minExtent = Math.max(0, brush.extent()[0]);
    var maxExtent = Math.min(lastEndTime, brush.extent()[1]);
    var visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});

    mini.select('.brush').call(brush.extent([minExtent, maxExtent]));
    x1.domain([minExtent, maxExtent]);
    main.select('.main.axis').call(x1DateAxis);

    function mouseoverTooltip(d) {
      var text = d.cmdLine ? htmlFormatCmdLine(d.cmdLine) : d.cmd;
      div.transition()
         .duration(50)
         .style("opacity", 1.0);
      div.html(text)
      mousemoveTooltip(d);
    }
    function mousemoveTooltip(d) {
      div.style("left", (d3.event.pageX) + "px")
         .style("top", (d3.event.pageY - 28) + "px");
    }
    function mouseoutTooltip(d) {
      div.transition()
          .duration(500)
          .style("opacity", 0);
    }
    // upate the item rects
    var rects = itemRects.selectAll('rect')
      .data(visItems, function (d) { return d.id; })
      .attr('x', function(d) { return x1(d.start); })
      .attr('width', function(d) { return x1(d.end) - x1(d.start); });

    rects.enter().append('rect')
      .attr('x', function(d) { return x1(d.start); })
      .attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
      .attr('width', function(d) { return x1(d.end) - x1(d.start); })
      .attr('height', function(d) { return .8 * y1(1); })
      .attr('class', function(d) { return 'mainItem ' + d.class; })
      .attr('fill', function(d) { return d.color; })
      .on("mouseover", mouseoverTooltip)
      .on("mousemove", mousemoveTooltip)
      .on("mouseout", mouseoutTooltip);
    rects.exit().remove();

    // update the item labels
    var labels = itemRects.selectAll('text')
      .data(visItems, function (d) { return d.id; })
      .attr('x', function(d) { return x1(Math.max(d.start, minExtent)) + 2; });

    labels.enter().append('text')
      .text(function (d) { return d.label; })
      .attr('x', function(d) { return x1(Math.max(d.start, minExtent)) + 2; })
      .attr('y', function(d) { return y1(d.lane) + .4 * y1(1) + 6.5; })
      .attr('text-anchor', 'start')
      .attr('class', 'itemLabel')
      .on("mouseover", mouseoverTooltip)
      .on("mousemove", mousemoveTooltip)
      .on("mouseout", mouseoutTooltip);

    labels.exit().remove();
  }
}

var emProfileJsonData = {{{ emprofile_json_data }}};
createSwimlaneChart(emProfileJsonData);

</script>
</body>
</html>
